/*
 * Copyright (C) 2012 DataSymphony (http://datasymphony.com.au)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package au.com.ds.restty.fmt;

import java.util.Map;

import au.com.ds.restty.cmd.Command;
import au.com.ds.restty.exc.FormattingError;
import au.com.ds.restty.exc.InvalidTemplate;
import au.com.ds.restty.js.JsAnyObject;
import au.com.ds.restty.js.JsArray;
import au.com.ds.restty.js.JsObject;
import au.com.ds.restty.js.JsObjectFormatter;
import au.com.ds.restty.js.JsObjectType;
import au.com.ds.restty.js.JsString;
import au.com.ds.restty.util.Utils;

public class JsonFormatter implements Formatter {
	private static final int SPACES_PER_LEVEL = 3;
	
	public static final String OPT_STRIP_QUOTES = "OPT_STRIP_QUOTES";
	
	private Command mCommand;
	private String m1Indent;
	private boolean mStripQuotes;
	
	protected JsonFormatter(Command cmd){
		mCommand = cmd;
		m1Indent = Utils.dupString(" ", SPACES_PER_LEVEL);
		mStripQuotes = true;
	}
	
	protected JsonFormatter(){
		m1Indent = Utils.dupString(" ", SPACES_PER_LEVEL);
		mStripQuotes = true;
	}

	@Override
	public String getFormattedResult() {
		return format(mCommand.getResultObject(), null);
	}

	@Override
	public String format(JsAnyObject obj, Map<String, Object> options) {
		if (options != null && options.get(OPT_STRIP_QUOTES) != null){
			try {
				mStripQuotes = Boolean.valueOf(options.get(OPT_STRIP_QUOTES).toString());				
			} catch (Exception e){
				throw new InvalidTemplate("Error applying formatter options: " + options, e);
			}
		}
		
		StringBuilder sb;
		try {
			sb = new StringBuilder();
			processObject(obj, sb, 0, true);
		} catch (Exception e){
			throw new FormattingError("Error during formatting JSON", e);
		}
		
		return sb.toString();
	}
	
	private void processObject(JsAnyObject o, StringBuilder sb, int level, boolean useInitialIndent){
		String currIndent = Utils.dupString(m1Indent, level);
		
		switch (o.getType()) {
		case ARRAY:
			JsArray a = (JsArray) o;
			int size = a.getSize(); 
			if (useInitialIndent){
				sb.append(currIndent);
			}
			if (size == 0){
				sb.append("[]");
			} else {
				sb.append("[").append(Utils.EOL);
				for (int i = 0; i < size; i++){
					JsAnyObject oo = a.get(i);
					if (oo.getType() != JsObjectType.OBJECT || useInitialIndent){
						sb.append(currIndent);
					}
					if (oo.getType() != JsObjectType.ARRAY && oo.getType() != JsObjectType.OBJECT){
						sb.append(m1Indent);
					}
					processObject(oo, sb, level + 1, true);
					if (i != size - 1){
						sb.append(",");
					}
					sb.append(Utils.EOL);
				}
				sb.append(currIndent).append("]");
			}
			break;
		case DOUBLE:
			sb.append(JsObjectFormatter
					.required()
					.expects(JsObjectType.DOUBLE)
					.format(o));
			break;
		case NULL:
			sb.append("null");
			break;
		case BOOLEAN:
			sb.append(JsObjectFormatter
						.required()
						.expects(JsObjectType.BOOLEAN)
						.format(o));
			break;
		case OBJECT:
			JsObject jso = (JsObject) o;
			size = jso.getSize(); 
			if (useInitialIndent){
				sb.append(currIndent);
			}
			if (size == 0){
				sb.append("{}");
			} else {
				sb.append("{").append(Utils.EOL);
				int i = 0;
				for (Map.Entry<JsAnyObject, JsAnyObject> e : jso.getValue().entrySet()){
					sb.append(currIndent).append(m1Indent);
					JsAnyObject key = e.getKey();
					JsAnyObject value = e.getValue();
					
					// process key
					String keyStr;
					if (key.getType() == JsObjectType.STRING){
						JsString key_ = (JsString) key;
						keyStr = key_.getValue();
						if (!Utils.isValidJsIdentifier(keyStr) || !mStripQuotes){
							keyStr = "\"" + keyStr + "\"";
						}
					} else if (key.getType() == JsObjectType.DOUBLE){
						keyStr = JsObjectFormatter
									.required()
									.expects(JsObjectType.DOUBLE)
									.format(key);
					} else {
						// should never happen
						throw new RuntimeException("Unsupported key type: " + key.getType());
					}
					
					sb.append(keyStr).append(" : ");
					
					processObject(value, sb, level + 1, false);
					if (i++ != size - 1){
						sb.append(",");
					}
					sb.append(Utils.EOL);
				}
				sb.append(currIndent).append("}");
			}
			break;
		case STRING:
		case DATE:
			sb.append("\"");
			sb.append(JsObjectFormatter
							.required()
							.format(o));
			sb.append("\"");
			break;
		case UNDEFINED:
			sb.append("undefined");
			break;
		}
	}
}
